package ca.usherbrooke.domus.sipnmove.sip.impl;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogState;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.PeerUnavailableException;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.SipStack;
import javax.sip.TimeoutEvent;
import javax.sip.TransactionAlreadyExistsException;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.TransactionUnavailableException;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.address.URI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderFactory;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.MessageFactory;
import javax.sip.message.Request;
import javax.sip.message.Response;

import ca.usherbrooke.domus.sipnmove.sip.SipFacade;
import ca.usherbrooke.domus.sipnmove.sip.SipFacadeListener;
import ca.usherbrooke.domus.sipnmove.sip.SipUserInformation;

/**
 * Implementation of the SIP facade
 */
public class SipFacadeImpl implements SipFacade, SipListener {

	/**
	 * Transport to use under SIP
	 * 
	 * Tested with "tcp" and should work with "udp" as well
	 */
	private static final String transport = "tcp";

	/**
	 * SIP facade listeners map
	 * 
	 * Keys are callId
	 */
	private Map<String, SipFacadeListener> sipFacadeListeners;

	/**
	 * SIP Header factory
	 */
	private HeaderFactory headerFactory;

	/**
	 * SIP Messages factory
	 */
	private MessageFactory messageFactory;

	/**
	 * SIP address factory
	 */
	private AddressFactory addressFactory;

	/**
	 * Jain-Sip provider
	 */
	private SipProvider sipProvider;

	/**
	 * SIP user information
	 */
	private SipUserInformation sipUserInformation;

	/**
	 * Unknown call sip facade listener
	 * 
	 * Handle message that have no call recipient (unknown CallId)
	 */
	private SipFacadeListener unknownCallSipFacadeListener;

	/**
	 * Default constructor
	 */
	public SipFacadeImpl(String ipAddress, Integer port, String userName) {
		sipFacadeListeners = new HashMap<String, SipFacadeListener>();

		// FIXME: Seems strange to define the user name here, because user can
		// change dynamically (by moving)
		sipUserInformation = new SipUserInformation();
		sipUserInformation.setIpAddress(ipAddress);
		sipUserInformation.setPort(port);
		sipUserInformation.setName(userName);

		initSip(ipAddress, port);
	}

	/**
	 * Initialize SIP sub layers
	 * 
	 * @param ipAddress
	 * @param port
	 */
	private void initSip(String ipAddress, Integer port) {
		SipFactory sipFactory = SipFactory.getInstance();
		sipFactory.setPathName("gov.nist");

		// The port number is used as a Unique ID
		Properties properties = new Properties();
		properties.setProperty("javax.sip.STACK_NAME", "useragent" + port);
		// You need 16 for logging traces. 32 for debug + traces.
		// Your code will limp at 32 but it is best for debugging.
		properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "32");
		properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "ua" + port + "_debug.txt");
		properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "ua" + port + "_log.txt");

		try {

			// Create SipStack object
			SipStack sipStack = sipFactory.createSipStack(properties);

			headerFactory = sipFactory.createHeaderFactory();
			addressFactory = sipFactory.createAddressFactory();
			messageFactory = sipFactory.createMessageFactory();

			ListeningPoint listeningPoint = sipStack.createListeningPoint(ipAddress, port, transport);

			sipProvider = sipStack.createSipProvider(listeningPoint);
			sipProvider.addSipListener(this);

		} catch (PeerUnavailableException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void addSipFacadeListener(String callId, SipFacadeListener sipFacadeListener) {
		sipFacadeListeners.put(callId, sipFacadeListener);
	}

	@Override
	public String generateCallId() {
		CallIdHeader callIdHeader = sipProvider.getNewCallId();
		return callIdHeader.getCallId();
	}

	@Override
	public void sendAck() {
		/*
		 * Response response = responseEvent.getResponse(); CSeqHeader
		 * cSeqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME); Dialog
		 * dialog = responseEvent.getDialog(); try { Request ackRequest =
		 * dialog.createAck(((CSeqHeader) cSeqHeader).getSeqNumber());
		 * dialog.sendAck(ackRequest);
		 * 
		 * } catch (SipException e) { e.printStackTrace(); } catch
		 * (InvalidArgumentException e) { e.printStackTrace(); }
		 */
	}

	@Override
	public void sendBye() {
		// TODO Auto-generated method stub

	}

	@Override
	public void sendInvite(String toAddressString, String callId) {

		try {

			// create From Header
			Address fromAddress = addressFactory.createAddress(sipUserInformation.getSipAddress());
			FromHeader fromHeader = headerFactory.createFromHeader(fromAddress, "12345");

			// create To Header
			Address toAddress = addressFactory.createAddress(toAddressString);
			ToHeader toHeader = headerFactory.createToHeader(toAddress, null);

			// create Request URI
			URI requestURI = addressFactory.createAddress(toAddressString).getURI();

			// Create ViaHeaders
			List<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
			ViaHeader viaHeader = headerFactory.createViaHeader(sipUserInformation.getIpAddress(), sipUserInformation.getPort(), transport, null);
			viaHeaders.add(viaHeader);

			// Create ContentTypeHeader
			ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "sdp");

			// Create a new CallId header
			CallIdHeader callIdHeader = headerFactory.createCallIdHeader(callId);

			// Create a new Cseq header
			CSeqHeader cSeqHeader = headerFactory.createCSeqHeader(1L, Request.INVITE);

			// Create a new MaxForwardsHeader
			MaxForwardsHeader maxForwards = headerFactory.createMaxForwardsHeader(70);

			// Create the request.
			Request request = messageFactory.createRequest(requestURI, Request.INVITE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);

			// Create contact headers
			Address contactAddress = addressFactory.createAddress(sipUserInformation.getSipAddress());
			request.addHeader(headerFactory.createContactHeader(contactAddress));

			// You can add extension headers of your own making
			// to the outgoing SIP request.
			// Add the extension header.
			// Header extensionHeader = headerFactory.createHeader("My-Header",
			// "my header value");
			// request.addHeader(extensionHeader);

			// This is an example SDP content, but it will not be parsed in our
			// case because of time

			String sdpData = "v=0\r\n" + "o=4855 13760799956958020 13760799956958020" + " IN IP4  129.6.55.78\r\n" + "s=mysession session\r\n" + "p=+46 8 52018010\r\n" + "c=IN IP4  129.6.55.78\r\n" + "t=0 0\r\n" + "m=audio 6022 RTP/AVP 0 4 18\r\n" + "a=rtpmap:0 PCMU/8000\r\n" + "a=rtpmap:4 G723/8000\r\n" + "a=rtpmap:18 G729A/8000\r\n" + "a=ptime:20\r\n";
			byte[] contents = sdpData.getBytes();
			request.setContent(contents, contentTypeHeader);

			// Create the client transaction and send the request
			ClientTransaction clientTransaction = sipProvider.getNewClientTransaction(request);
			
			clientTransaction.sendRequest();

		} catch (SipException e) {
			e.printStackTrace();
		} catch (InvalidArgumentException e) {
			e.printStackTrace();
		} catch (ParseException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void sendOk(/*
						 * RequestEvent requestEvent, ServerTransaction
						 * serverTransaction,SipUserInformation userInfos
						 */) {
		/*
		 * SipProvider sipProvider = (SipProvider) requestEvent.getSource();
		 * Request request = requestEvent.getRequest();
		 * 
		 * try {
		 * 
		 * if (serverTransaction == null) { serverTransaction =
		 * sipProvider.getNewServerTransaction(requestEvent.getRequest()); }
		 * 
		 * Response okResponse = messageFactory.createResponse(Response.OK,
		 * request); Address address =
		 * addressFactory.createAddress(userInfos.getSipAddress());
		 * ContactHeader contactHeader =
		 * headerFactory.createContactHeader(address); ToHeader toHeader =
		 * (ToHeader) okResponse.getHeader(ToHeader.NAME);
		 * toHeader.setTag("4321"); okResponse.addHeader(contactHeader);
		 * 
		 * serverTransaction.sendResponse(okResponse);
		 * 
		 * } catch (Exception ex) { ex.printStackTrace(); System.exit(0); }
		 */
	}

	@Override
	public void sendRinging() {
		// TODO Auto-generated method stub

	}

	@Override
	public void processDialogTerminated(DialogTerminatedEvent arg0) {
		// TODO Auto-generated method stub

	}

	@Override
	public void processIOException(IOExceptionEvent arg0) {
		// TODO Auto-generated method stub

	}

	@Override
	public void processRequest(RequestEvent requestEvent) {

		try {
			Request request = requestEvent.getRequest();
			ServerTransaction serverTransaction = requestEvent.getServerTransaction();

			if (serverTransaction == null) {
				serverTransaction = sipProvider.getNewServerTransaction(requestEvent.getRequest());
			}

			String callId = serverTransaction.getDialog().getCallId().getCallId();

			if (request.getMethod().equals(Request.INVITE)) {
				notifyInvite(callId);
			} else if (request.getMethod().equals(Request.ACK)) {
				notifyAck(callId);
			} else if (request.getMethod().equals(Request.BYE)) {
				notifyBye(callId);
			} else if (request.getMethod().equals(Request.CANCEL)) {
				notifyCancel(callId);
			} else {
				System.err.println("Unknown SIP method");
			}

		} catch (TransactionAlreadyExistsException e) {
			e.printStackTrace();
		} catch (TransactionUnavailableException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void processResponse(ResponseEvent responseEvent) {
		String callId = responseEvent.getDialog().getCallId().getCallId();
		Response response = responseEvent.getResponse();
		CSeqHeader cSeqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
		Dialog dialog = responseEvent.getDialog();

		if (response.getStatusCode() == Response.OK) {
			if (cSeqHeader.getMethod().equals(Request.INVITE)) {
				// notifyOk(responseEvent);

			} else if (cSeqHeader.getMethod().equals(Request.CANCEL)) {
				if (dialog.getState() == DialogState.CONFIRMED) {
					try {

						Request byeRequest = dialog.createRequest(Request.BYE);

						ClientTransaction ct = sipProvider.getNewClientTransaction(byeRequest);
						dialog.sendRequest(ct);

					} catch (SipException e) {
						e.printStackTrace();
					}

				}

			}
		}

	}

	@Override
	public void processTimeout(TimeoutEvent arg0) {
		// TODO Auto-generated method stub

	}

	@Override
	public void processTransactionTerminated(TransactionTerminatedEvent arg0) {
		// TODO Auto-generated method stub

	}

	@Override
	public void removeSipFacadeListener(String callId) {
		if (sipFacadeListeners.containsKey(callId))
			sipFacadeListeners.remove(callId);
	}

	@Override
	public void setUnknownCallSipFacadeListener(SipFacadeListener sipFacadeListener) {
		this.unknownCallSipFacadeListener = sipFacadeListener;
	}

	@Override
	public SipUserInformation getSipUserInformation() {
		return this.sipUserInformation;
	}

	/**
	 * Resolve the facade listener that should handle the message thanks to the
	 * SIP CallId value
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private SipFacadeListener resolveSipFacadeListener(String callId) {
		if (sipFacadeListeners.containsKey(callId))
			return sipFacadeListeners.get(callId);

		return unknownCallSipFacadeListener;
	}

	/**
	 * Notify listeners that a TRYING message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyTrying(String callId) {
		resolveSipFacadeListener(callId).processTrying(callId);
	}

	/**
	 * Notify listeners that a RINGING message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyRinging(String callId) {
		resolveSipFacadeListener(callId).processRinging(callId);
	}

	/**
	 * Notify listeners that a OK message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyOk(String callId) {
		resolveSipFacadeListener(callId).processOk(callId);
	}

	/**
	 * Notify listeners that a ACK message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyAck(String callId) {
		resolveSipFacadeListener(callId).processAck(callId);
	}

	/**
	 * Notify listeners that a INVITE message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyInvite(String callId) {
		resolveSipFacadeListener(callId).processInvite(callId);
	}

	/**
	 * Notify listeners that a BYE message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyBye(String callId) {
		resolveSipFacadeListener(callId).processBye(callId);
	}

	/**
	 * Notify listeners that a CANCEL message has income
	 * 
	 * @param callId
	 *            SIP CallId
	 */
	private void notifyCancel(String callId) {
		resolveSipFacadeListener(callId).processCancel(callId);
	}
}
